﻿using CivitaiAPI;
using CivitaiAPI.Utils;
using CivitaiCLI.Utils;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Windows.Forms;

namespace CivitaiCLI
{
    class Program
    {
        static OperatingMode[] ignoreModesListing = new OperatingMode[] { OperatingMode.Invalid, OperatingMode.Interactive };

        static volatile bool failed = false;
        static string failReason = null;

        static bool simulate = false;
        static bool verbose = false;
        static string modelPattern = "{name}_{version}";
        static string filePattern = "{name}_{version}";
        static string pathPattern = "{id}_{name}";
        static OperatingMode mode = OperatingMode.Interactive;
        static ClipboardMode clipboardMode = ClipboardMode.Download;
        static ModelPreference modelPreference;
        static int imageResolution = 450;
        static TimeSpan monitorDelay = new TimeSpan(0, 5, 0);
        static int monitorModelsCount = 50;
        static bool monitorAlwaysUpdate = false;
        static bool downloadOnlyMetadata = false;
        static int downloadTimeout = 30000;
        static string authToken = null;
        static string userAgent = null;
        static TimeSpan serviceUnavailableDelay = new TimeSpan(0, 0, 5);
        static List<string> includeType = new List<string>();
        static List<string> includeTag = new List<string>();
        static List<string> excludeType = new List<string>();
        static List<string> excludeTag = new List<string>();

        static ConsoleColor InfoColor = ConsoleColor.DarkYellow;
        static ConsoleColor ErrorColor = ConsoleColor.DarkRed;
        static ConsoleColor SuccessColor = ConsoleColor.DarkGreen;
        static ConsoleColor KeyColor = ConsoleColor.DarkGray;
        static ConsoleColor ValueColor = Console.ForegroundColor;
        static ConsoleColor WarnColor = ConsoleColor.DarkMagenta;

        static string workingDir;

        static CivitaiCLISettings settings;
        static SettingsManager<CivitaiCLISettings> settingsMan = new SettingsManager<CivitaiCLISettings>(nameof(CivitaiCLISettings), createBackups: false);

        [STAThread]
        static void Main(string[] args)
        {
            Console.OutputEncoding = Encoding.UTF8;
            CText.DefaultColor = ValueColor;

            void InvalidPath(string path)
            {
                ConsoleUtil.WriteLine($"Invalid path supplied: \"{path}\", reverting to default: \"{Environment.CurrentDirectory}\"", ErrorColor);
                workingDir = Environment.CurrentDirectory;
            }

            settings = settingsMan.LoadSettings();
            settingsMan.SaveSettings(settings);

            CommandLineDictionary d = CommandLineDictionary.FromArguments(args, '/', '=');
            workingDir = d.GetValue("path", settings.HasPath ? settings.Path : Environment.CurrentDirectory);
            if(!IOUtil.TryFormatAbsolutePath(ref workingDir))
            {
                InvalidPath(workingDir);
            }

            if (d.ContainsKey("?") || d.ContainsKey("help"))
            {
                PrintHelp();
            }
            else
            {
                simulate = d.ContainsKey("s") || d.ContainsKey("simulate");
                verbose = d.ContainsKey("v") || d.ContainsKey("verbose");

                modelPattern = d.GetValue("modelpattern", modelPattern);
                pathPattern = d.GetValue("pathpattern", pathPattern);

                if (d.ContainsKey("imagewidth"))
                    imageResolution = d.GetValueInt("imagewidth", imageResolution);


                if (d.ContainsKey("monitordelay"))
                {
                    int monitorDelayResult = d.GetValueInt("monitordelay", (int)Math.Round(monitorDelay.TotalSeconds));
                    if (monitorDelayResult > 3599)
                        monitorDelayResult = 3599;
                    monitorDelay = new TimeSpan(0, 0, monitorDelayResult);
                }

                if (d.ContainsKey("monitormodelcount") && int.TryParse(d["monitormodelcount"], out int monitorModelCountResult) && monitorModelCountResult > 0)
                    monitorModelsCount = monitorModelCountResult;

                monitorAlwaysUpdate = d.ContainsKey("monitoralwaysupdate") ? d.GetValueBool("monitoralwaysupdate", true) : settings.MonitorAlwaysUpdate;

                downloadOnlyMetadata = d.ContainsKey("downloadonlymetadata") ? d.GetValueBool("downloadonlymetadata", true) : settings.DownloadOnlyMetadata;

                if (d.ContainsKey("downloadtimeout") && int.TryParse(d["downloadtimeout"], out int downloadTimeoutResult) && downloadTimeoutResult > 0)
                {
                    downloadTimeout = downloadTimeoutResult * 1000;
                    if(downloadTimeout > 60000)
                        ConsoleUtil.WriteLine($"Setting downloadtimeout to: {TimeSpan.FromMilliseconds(downloadTimeout).ShortFormat()} this might be in error, if not you can ignore this warning", WarnColor);
                }

                authToken = d.GetValue("authtoken", settings.HasAuthToken ? settings.AuthToken : null);
                userAgent = d.GetValue("useragent", settings.HasUserAgent ? settings.UserAgent : null);

                string modeStr = "";
                if (d.ContainsKey("mode"))
                {
                    modeStr = d["mode"];
                    if (!Enum.TryParse(modeStr, true, out OperatingMode outMode))
                        outMode = OperatingMode.Invalid;

                    mode = outMode;
                }
                else
                {
                    mode = settings.Mode;
                }

                string clipboardModeStr = "";
                if (d.ContainsKey("clipboardmode"))
                {
                    clipboardModeStr = d["clipboardmode"];
                    if (!Enum.TryParse(clipboardModeStr, true, out ClipboardMode outMode))
                        outMode = ClipboardMode.Download;

                    clipboardMode = outMode;
                }
                else
                {
                    clipboardMode = settings.ClipboardMode;
                }

                modelPreference = ModelPreference.None;
                string preferenceString = d.GetValue("modelpreference", "none");
                string[] splitPrefs = preferenceString.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
                for (int i = 0; i < splitPrefs.Length; i++)
                {
                    if (Enum.TryParse(splitPrefs[i], true, out ModelPreference outPref))
                    {
                        modelPreference |= outPref;
                    }
                }

                if (d.ContainsKey("includetype"))
                    includeType.AddRange(d["includetype"].Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries));

                if (d.ContainsKey("excludetype"))
                    excludeType.AddRange(d["excludetype"].Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries));

                if (d.ContainsKey("includetag"))
                    includeTag.AddRange(d["includetag"].Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries));

                if (d.ContainsKey("excludetag"))
                    excludeTag.AddRange(d["excludetag"].Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries));

                if ((includeType.Count > 0 && excludeType.Count > 0) || (includeTag.Count > 0 && excludeTag.Count > 0))
                {
                    failReason = $"You can't specify both a include and exclude list, these are mutually exclusive";
                    failed = true;
                }

                if (!failed)
                {
                    if (mode == OperatingMode.Interactive)
                        mode = InteractiveMode();

                    if (mode == OperatingMode.Download)
                    {
                        RunDownloadMode();
                    }
                    else if (mode == OperatingMode.Clipboard)
                    {
                        RunClipboardMode();
                    }
                    else if (mode == OperatingMode.Monitor)
                    {
                        RunMonitorMode();
                    }
                    else if (mode == OperatingMode.Validate)
                    {
                        RunValidateMode();
                    }
                    else if (mode == OperatingMode.Invalid)
                    {
                        failed = true;
                    }
                    else
                    {
                        ConsoleUtil.WriteLine($"Mode: \"{modeStr}\" not supported", ErrorColor);
                        PrintHelp();
                        failed = true;
                    }
                }
            }

            if (failed)
            {
                if (failReason != null)
                    ConsoleUtil.WriteLine(failReason, ErrorColor);
                Console.ReadKey(true);
            }
        }
        //-------------------------------------------------------------
        //
        //-------------------------------------------------------------
        static OperatingMode InteractiveMode()
        {
            ConsoleUtil.WriteLine("No mode selected, pass \"/mode=somemode\" as arguments to skip this prompt or \"/?\" for help", KeyColor);
            ConsoleUtil.WriteLine("Select a mode", KeyColor);
            Array types = Enum.GetValues(typeof(OperatingMode));
            List<OperatingMode> choices = new List<OperatingMode>();
            for (int i = 0; i < types.Length; i++)
            {
                OperatingMode type = (OperatingMode)types.GetValue(i);
                if (!ignoreModesListing.Contains(type))
                {
                    choices.Add(type);
                    ConsoleUtil.Write($"{choices.Count}", SuccessColor);
                    ConsoleUtil.WriteLine($": {type.ToString()}", ValueColor);
                }
            }
            int selection = -1;
            while(selection == -1)
            {
                var key = Console.ReadKey(true);
                if (int.TryParse(new string(key.KeyChar, 1), out int iKey) && iKey > 0 && iKey <= choices.Count)
                {
                    selection = iKey;
                }
                else if(key.KeyChar == '?')
                {
                    PrintHelp();
                    return OperatingMode.Invalid;
                }
                else
                {
                    ConsoleUtil.WriteLine($"Bad Selection", ErrorColor);
                }
            }
            return choices[selection - 1]; 
        }

        //-------------------------------------------------------------
        //
        //-------------------------------------------------------------
        static void RunDownloadMode()
        {
            PrintModeInfo();

            ConsoleUtil.WriteLine("Paste in a Civitai model url and hit enter", InfoColor);
            while (!failed)
            {
                string line = Console.ReadLine();
                Console.CursorVisible = false;
                try
                {
                    ModelInfo model = null;
                    if (Uri.TryCreate(line, UriKind.RelativeOrAbsolute, out Uri outUri) && outUri.IsAbsoluteUri)
                    {
                        using (var b = AsyncHelper.Wait)
                        {
                            b.Run(CivitaiDownloader.GetModelInfoByUrlAsync(outUri), res => model = res);
                        }

                        if (model != null)
                        {
                            int downloadVersion = -1;
                            if (CivitaiDownloader.TryParseModelVersion(outUri.Query, out int modelVersionID))
                                downloadVersion = modelVersionID;

                            ConsoleUtil.Write("Found model: ", KeyColor);
                            ConsoleUtil.WriteLine($"{model.name}", ValueColor);
                            if (downloadVersion != -1)
                            {
                                ConsoleUtil.Write("Specified Version: ", KeyColor);
                                ConsoleUtil.WriteLine($"{downloadVersion}", ValueColor);
                            }

                            bool success = DownloadModel(model, downloadVersion);

                            ConsoleUtil.WriteLine($"Done{(success ? "" : " (An error occurred)")}", success ? SuccessColor : ErrorColor);
                            ConsoleUtil.Write("Waiting for next model url", InfoColor);
                            Console.WriteLine();
                        }
                        else
                        {
                            ConsoleUtil.WriteLine("Not a valid Civitai model url?", ErrorColor);
                        }
                    }
                    else
                    {
                        ConsoleUtil.Write("Invalid Url", ErrorColor);
                        Console.WriteLine();
                    }
                }
                catch (Exception ex)
                {
                    if (ex is AggregateException aEx)
                    {
                        foreach (var aExItem in aEx.InnerExceptions)
                        {
                            ConsoleUtil.WriteLine($"{aExItem.Message}", ErrorColor);
                        }
                    }
                    else
                    {
                        ConsoleUtil.WriteLine($"{ex.Message}", ErrorColor);
                    }
                }
                Console.CursorVisible = true;
            }
        }

        //-------------------------------------------------------------
        //
        //-------------------------------------------------------------
        static void RunClipboardMode()
        {
            Console.CursorVisible = false;
            PrintModeInfo();
            MessageOnlyWindow clipboard = null;
            bool running = true;
            bool paused = false;
            bool processingQueue = clipboardMode == ClipboardMode.Download;
            ConcurrentQueue<Tuple<int, int>> queue = new ConcurrentQueue<Tuple<int, int>>();
            SemaphoreSlim semaphoreWriteLine = new SemaphoreSlim(1);

            Console.CancelKeyPress += (s, e) =>
            {
                clipboard?.Invoke((MethodInvoker)delegate () { clipboard.Close(); });
                running = false;
                e.Cancel = true;
            };

            void handleClipboard(object sender, ClipboardChangedEventArgs e)
            {
                if (paused)
                    return;

                string[] lines = e.Text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
                for (int i = 0; i < lines.Length; i++)
                {
                    if (Uri.TryCreate(lines[i], UriKind.RelativeOrAbsolute, out Uri outUri) && outUri.IsAbsoluteUri)
                    {
                        if (CivitaiDownloader.TryParseModelUrl(outUri, out int id))
                        {
                            int modelVersion = -1;
                            if (CivitaiDownloader.TryParseModelVersion(outUri.Query, out int modelVersionID))
                                modelVersion = modelVersionID;

                            semaphoreWriteLine.Wait();
                            ConsoleUtil.Write("Added: ", InfoColor);
                            ConsoleUtil.Write(lines[i], ValueColor);
                            Console.WriteLine();
                            semaphoreWriteLine.Release();

                            queue.Enqueue(new Tuple<int, int>(id, modelVersion));
                        }
                    }
                }
            }

            var threadStart = new ThreadStart(() =>
            {
                clipboard = new MessageOnlyWindow();
                clipboard.ClipboardChanged += handleClipboard;
                Application.Run(clipboard);
                clipboard.ClipboardChanged -= handleClipboard;
            });

            var thread = new Thread(threadStart);
            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();

            ConsoleUtil.Write($"Listening for clipboard events, hotkeys: ", ValueColor);
            ConsoleUtil.Write("Q", SuccessColor);
            ConsoleUtil.Write("uit, ", ValueColor);
            ConsoleUtil.Write("P", SuccessColor);
            ConsoleUtil.Write("ause", ValueColor);

            if (clipboardMode == ClipboardMode.Queue)
            {
                ConsoleUtil.Write($" and ", ValueColor);
                ConsoleUtil.Write("Enter", SuccessColor);
                ConsoleUtil.Write(" to start queue", ValueColor);
            }

            Console.WriteLine();

            while (running)
            {
                Thread.Sleep(100);

                if (Console.KeyAvailable)
                {
                    var key = Console.ReadKey(true);
                    if (key.KeyChar == 'q')
                    {
                        ConsoleUtil.WriteLine($"quitting", ErrorColor);
                        clipboard?.Invoke((MethodInvoker)delegate () { clipboard.Close(); });
                        break;
                    }
                    else if (key.KeyChar == 'p')
                    {
                        paused = !paused;
                        ConsoleUtil.WriteLine(paused ? "Paused" : "Resumed", InfoColor);
                    }
                    else if (key.KeyChar == '\r' && clipboardMode == ClipboardMode.Queue && !processingQueue && !queue.IsEmpty)
                    {
                        processingQueue = true;
                        if (paused)
                        {
                            paused = false;
                            ConsoleUtil.WriteLine("Resumed", InfoColor);
                        }
                        ConsoleUtil.WriteLine($"Starting queue: {queue.Count} models", ValueColor);
                    }
                }

                if (processingQueue && !paused && running)
                {
                    if (queue.TryDequeue(out Tuple<int, int> modelID))
                    {
                        ModelInfo model = null;
                        using (var b = AsyncHelper.Wait)
                        {
                            b.Run(CivitaiDownloader.GetModelInfoByIDAsync(modelID.Item1), res => model = res);
                        }
                        if (model != null)
                        {
                            semaphoreWriteLine.Wait();
                            ConsoleUtil.Write("Found model: ", KeyColor);
                            ConsoleUtil.WriteLine($"{model.name}", ValueColor);
                            if(modelID.Item2 != -1)
                            {
                                ConsoleUtil.Write("Specified Version: ", KeyColor);
                                ConsoleUtil.WriteLine($"{modelID.Item2}", ValueColor);
                            }
                            bool success = DownloadModel(model, modelID.Item2);
                            ConsoleUtil.WriteLine($"Done{(success ? "" : " (An error occurred)")}", success ? SuccessColor : ErrorColor);
                            semaphoreWriteLine.Release();
                        }
                    }
                    else if(clipboardMode == ClipboardMode.Queue)
                    {
                        processingQueue = false;
                        ConsoleUtil.WriteLine("-----------------", KeyColor);
                        ConsoleUtil.WriteLine("Done with queue", SuccessColor);
                        ConsoleUtil.WriteLine("-----------------", KeyColor);
                    }
                }
            }

            Console.CursorVisible = true;
            thread.Join(TimeSpan.FromSeconds(1));
        }

        //-------------------------------------------------------------
        //
        //-------------------------------------------------------------
        static void RunMonitorMode()
        {
            Console.CursorVisible = false;
            PrintModeInfo();

            DateTime lastUpdate = DateTime.MinValue;

            while (!failed)
            {
                ModelsResult result = null;
                DateTime updateStart = DateTime.UtcNow;

                try
                {
                    Console.Write("\r--------------------------Polling API--------------------------");
                    using (var b = AsyncHelper.Wait)
                    {
                        b.Run(CivitaiDownloader.GetModels(page: 1, perPage: monitorModelsCount), res => result = res);
                    }
                    Console.WriteLine("\r---------------------------------------------------------------");

                }
                catch (Exception ex)
                {
                    if (ex.InnerException is WebException wEx && wEx.Status == WebExceptionStatus.ProtocolError && wEx.Response is HttpWebResponse webResponse) //  && (int)webResponse.StatusCode == 525
                    {
                        ConsoleUtil.WriteLine($"\r---------------------------------------------------------------", ErrorColor);
                        ConsoleUtil.WriteLine($"{webResponse.StatusDescription}({(int)webResponse.StatusCode})", ErrorColor);
                        ConsoleUtil.WriteLine($"---------------------------------------------------------------", ErrorColor);
                    }
                    else if (ex.InnerException is JsonException iJEx)
                    {
                        ConsoleUtil.WriteLine($"\r---------------------------------------------------------------", ErrorColor);
                        ConsoleUtil.WriteLine($"JSON Error: {iJEx.Message}", ErrorColor);
                        ConsoleUtil.WriteLine($"---------------------------------------------------------------", ErrorColor);
                    }
                    else if (ex.InnerException is Exception iEx)
                    {
                        ConsoleUtil.WriteLine($"\r---------------------------------------------------------------", ErrorColor);
                        ConsoleUtil.WriteLine($"{iEx.Message}", ErrorColor);
                        ConsoleUtil.WriteLine($"---------------------------------------------------------------", ErrorColor);
                    }
                    else if (ex.InnerException is JsonException jEx)
                    {
                        ConsoleUtil.WriteLine($"\r---------------------------------------------------------------", ErrorColor);
                        ConsoleUtil.WriteLine($"Error parsing json response: {ex.Message}", ErrorColor);
                        ConsoleUtil.WriteLine($"---------------------------------------------------------------", ErrorColor);
                    }
                    else
                    {
                        ConsoleUtil.WriteLine($"\r---------------------------------------------------------------", ErrorColor);
                        ConsoleUtil.WriteLine($"{ex.Message}", ErrorColor);
                        ConsoleUtil.WriteLine($"---------------------------------------------------------------", ErrorColor);
                    }
                }

                if (result != null)
                {
                    DateTime newUpdate = lastUpdate;

                    Dictionary<string, List<Tuple<ModelInfo, List<Tuple<bool, ModelVersion>>>>> updatedList = new Dictionary<string, List<Tuple<ModelInfo, List<Tuple<bool, ModelVersion>>>>>();

                    for (int i = result.items.Count - 1; i >= 0; i--)
                    {
                        ModelInfo item = result.items[i];
                        for (int m = 0; m < item.modelVersions.Count; m++)
                        {
                            ModelVersion model = item.modelVersions[m];
                            TimeSpan delta = model.updatedAt - lastUpdate;

                            bool update = delta.TotalDays > 0.0;

                            if (updatedList.TryGetValue(item.type, out List<Tuple<ModelInfo, List<Tuple<bool, ModelVersion>>>> type))
                            {
                                var oldItem = type.Find((f) => f.Item1 == item);
                                if (oldItem == null)
                                {
                                    oldItem = new Tuple<ModelInfo, List<Tuple<bool, ModelVersion>>>(item, new List<Tuple<bool, ModelVersion>>());
                                    type.Add(oldItem);
                                }

                                oldItem.Item2.Add(new Tuple<bool, ModelVersion>(update, model));
                            }
                            else
                            {
                                List<Tuple<ModelInfo, List<Tuple<bool, ModelVersion>>>> list = new List<Tuple<ModelInfo, List<Tuple<bool, ModelVersion>>>>
                                    {
                                        new Tuple<ModelInfo, List<Tuple<bool, ModelVersion>>>(item, new List<Tuple<bool, ModelVersion>> { new Tuple<bool, ModelVersion>(update, model) })
                                    };
                                updatedList.Add(item.type, list);
                            }

                            TimeSpan lastDelta = model.updatedAt - newUpdate;
                            if (lastDelta.TotalDays >= 0.0)
                                newUpdate = model.updatedAt;
                        }
                    }

                    foreach (var item in updatedList.OrderByDescending(x => x.Key))
                    {
                        ConsoleUtil.Write(item.Key, KeyColor);
                        if (SkipFromTypesMatch(item.Key))
                        {
                            ConsoleUtil.WriteLine(" Skipped(Type)", WarnColor);
                            continue;
                        }
                        Console.WriteLine();

                        for (int i = 0; i < item.Value.Count; i++)
                        {
                            var model = item.Value[i];
                            for (int j = 0; j < model.Item2.Count; j++)
                            {
                                var version = model.Item2[j];
                                var updated = version.Item1 || monitorAlwaysUpdate;
                                ConsoleUtil.Write($"{model.Item1.name}_{version.Item2.name}", updated ? SuccessColor : ErrorColor);

                                if (SkipFromTagMatch(model.Item1.tags))
                                {
                                    ConsoleUtil.WriteLine(" Skipped(Tag)", WarnColor);
                                    continue;
                                }
                                Console.WriteLine();

                                //CivitaiDownloader.GetModelFiles(version.Item2, modelPreference);
                                if (updated)
                                {
                                    string path = FormatModelPath(model.Item1);
                                    if (!Directory.Exists(path) && !simulate)
                                        Directory.CreateDirectory(path);

                                    DownloadModelVersion(model.Item1, version.Item2, path, downloadOnlyMetadata);

                                    if (!simulate) //!hasGeneralDesc && 
                                    {
                                        if (model.Item1.HasDescription())
                                        {
                                            string generalPath = Path.Combine(path, "general_description.txt");
                                            if (!File.Exists(generalPath))
                                                File.WriteAllText(generalPath, model.Item1.FormatDescriptionText());
                                        }
                                        string modelMetaPath = Path.Combine(path, "model_metadata.txt");
                                        if (!File.Exists(modelMetaPath))
                                            File.WriteAllText(modelMetaPath, model.Item1.FormatModelMeta());
                                    }
                                }
                            }
                        }
                    }

                    lastUpdate = newUpdate;
                }

                TimeSpan updateDuration = DateTime.UtcNow - updateStart;
                if (updateDuration < monitorDelay)
                {
                    TimeSpan newWaitTime = monitorDelay - updateDuration;
                    //while(newWaitTime.TotalDays > 0.0)
                    //{
                    Console.Write($"\rUpdate Took: {updateDuration.ShortFormat()}, waiting for: {newWaitTime.ShortFormat(true)}");
                    //TimeSpan sleepTime = MathUtils.Clamp(newWaitTime, TimeSpan.Zero, new TimeSpan(0, 1, 0));
                    //Console.Write($" - Sleeping for: {sleepTime.ShortFormat()}");
                    Thread.Sleep(newWaitTime);
                    //newWaitTime = newWaitTime.Subtract(new TimeSpan(0, 1, 0));
                    //}
                    //Console.WriteLine();
                }
                else
                {
                    Console.Write($"\rUpdate Took: {updateDuration.ShortFormat()}");
                }
            }

            Console.CursorVisible = true;
        }

        //-------------------------------------------------------------
        //
        //-------------------------------------------------------------
        static void RunValidateMode()
        {
            Console.CursorVisible = false;
            PrintModeInfo();

            int found = 0;
            int notfound = 0;
            int error = 0;

            DirectoryInfo workingInfo = new DirectoryInfo(workingDir);
            DirectoryInfo nukedFolder = new DirectoryInfo(Path.Combine(workingDir, "Nuked"));

            DirectoryInfo[] folders = workingInfo.GetDirectories();
            for (int i = 0; i < folders.Length; i++)
            {
                string typeFolderName = folders[i].Name;
                if (typeFolderName.Equals("nuked", StringComparison.InvariantCultureIgnoreCase))
                    continue;

                ConsoleUtil.Write($"{typeFolderName}", KeyColor);
                if (SkipFromTypesMatch(typeFolderName))
                {
                    ConsoleUtil.WriteLine($" Skipped", WarnColor);
                    continue;
                }
                Console.WriteLine();

                DirectoryInfo nukedTypeFolder = new DirectoryInfo(Path.Combine(nukedFolder.FullName, typeFolderName));
                Stack<DirectoryInfo> processFolders = new Stack<DirectoryInfo>(folders[i].GetDirectories());
                while(processFolders.Count > 0)
                {
                    DirectoryInfo dir = processFolders.Pop();
                    string dirName = dir.Name;
                    if (TryGetModelIDFromModelMetadata(dir.FullName, out int id) || TryGetModelIDFromFolder(dirName, out id))
                    {
                        ConsoleUtil.Write($"{id} - ", ValueColor);

                        ModelInfo result = null;
                        string errorMessage = "";
                        bool removed = false;
                        HttpStatusCode statusCode = HttpStatusCode.OK;
                        try
                        {
                            using (var b = AsyncHelper.Wait)
                            {
                                b.Run(CivitaiDownloader.GetModelInfoByIDAsync(id), res => result = res);
                            }
                        }
                        catch (Exception ex)
                        {
                            if (ex.InnerException != null)
                            {
                                if (ex.InnerException is WebException wEx && wEx.Response is HttpWebResponse wResponse)
                                {
                                    statusCode = wResponse.StatusCode;
                                    if (statusCode == HttpStatusCode.NotFound)
                                    {
                                        removed = true;
                                        notfound++;
                                    }
                                    else if (statusCode != HttpStatusCode.ServiceUnavailable)
                                    {
                                        error++;
                                    }

                                    errorMessage = Enum.GetName(typeof(HttpStatusCode), statusCode);
                                }
                                else
                                {
                                    error++;
                                    errorMessage = ex.InnerException.Message;
                                }
                            }
                            else
                            {
                                errorMessage = ex.Message;
                                error++;
                            }
                        }

                        if (result != null)
                        {
                            found++;
                            ConsoleUtil.Write($"Found", SuccessColor);
                        }
                        else
                        {
                            if (statusCode == HttpStatusCode.ServiceUnavailable)
                            {
                                ConsoleUtil.Write($"{errorMessage} - Waiting for {serviceUnavailableDelay.ShortFormat()}", ErrorColor);
                                Thread.Sleep(serviceUnavailableDelay);
                                processFolders.Push(dir);
                            }
                            else
                            {
                                ConsoleUtil.Write($"{errorMessage}", ErrorColor);
                                if (removed)
                                {
                                    if (!simulate)
                                    {
                                        if(!nukedTypeFolder.Exists)
                                            nukedTypeFolder.Create();

                                        if (!nukedFolder.Exists)
                                            nukedFolder.Create();

                                        dir.MoveTo(Path.Combine(nukedTypeFolder.FullName, dirName));
                                    }
                                }
                            }
                        }

                        Console.WriteLine();
                    }
                    else
                    {
                        error++;
                        ConsoleUtil.Write($"{dir.Name} - ", ValueColor);
                        ConsoleUtil.Write($"Failed to parse ID", ErrorColor);
                        Console.WriteLine();
                    }
                }
            }

            Console.WriteLine("---------------------------------------------------------------");
            Console.WriteLine($"Found: {found}");
            Console.WriteLine($"NotFound: {notfound}");
            Console.WriteLine($"Error: {error}");
            Console.WriteLine("---------------------------------------------------------------");
            Console.CursorVisible = true;

            Console.ReadKey(true);
        }

        static bool TryGetModelIDFromFolder(string dirName, out int id)
        {
            int idResult = -1;
            int firstUnderscore = dirName.IndexOf('_');
            if(firstUnderscore != -1)
            {
                string idFromDir = dirName.Substring(0, firstUnderscore);
                if (int.TryParse(idFromDir, out int idOut))
                {
                    idResult = idOut;
                }
            }

            id = idResult;
            return idResult != -1;
        }

        static bool TryGetModelIDFromModelMetadata(string fullPath, out int id)
        {
            int idResult = -1;
            string metadataPath = Path.Combine(fullPath, "model_metadata.txt");
            if (File.Exists(metadataPath))
            {
                using (var file = File.OpenRead(metadataPath))
                using (var reader = new StreamReader(file))
                {
                    string line;
                    while ((line = reader.ReadLine()) != null)
                    {
                        int idIndex = line.IndexOf("ID: ");
                        if (idIndex == 0)
                        {
                            if (int.TryParse(line.Substring(4), out int idOut))
                            {
                                idResult = idOut;
                            }
                            break;
                        }
                    }
                }
            }

            id = idResult;
            return idResult != -1;
        }

        static bool DownloadModel(ModelInfo model, int modelID = -1)
        {
            bool result = true;

            string path = FormatModelPath(model);
            if (!Directory.Exists(path) && !simulate)
                Directory.CreateDirectory(path);

            bool fetchModelVersion = modelID > 0;

            for (int i = 0; i < model.modelVersions.Count; i++)
            {
                ModelVersion version = model.modelVersions[i];
                ConsoleUtil.Write($"Model version: ", KeyColor);
                ConsoleUtil.Write($"{version.id} - {version.name} - ", ValueColor);

                if (fetchModelVersion && modelID != version.id)
                {
                    ConsoleUtil.WriteLine($"Skipping", WarnColor);
                    continue;
                }
                else
                {
                    ConsoleUtil.WriteLine($"Downloading", SuccessColor);
                }

                if(!DownloadModelVersion(model, version, path, downloadOnlyMetadata))
                {
                    result = false;
                }
            }

            if (!simulate)
            {
                if (model.HasDescription())
                {
                    string generalPath = Path.Combine(path, "general_description.txt");
                    if (!File.Exists(generalPath))
                        File.WriteAllText(generalPath, model.FormatDescriptionText());
                }
                string modelMetaPath = Path.Combine(path, "model_metadata.txt");
                if (!File.Exists(modelMetaPath))
                    File.WriteAllText(modelMetaPath, model.FormatModelMeta());
            }

            return result;
        }

        static bool DownloadModelVersion(ModelInfo model, ModelVersion version, string path, bool onlyMetadata = false)
        {
            bool result = true;

            string versionName = FormatModelName(model, version);

            string versionInfoPath = Path.Combine(path, versionName + ".txt");

            if (!File.Exists(versionInfoPath) && !simulate)
            {
                string desc = "";
                if (version.HasDescription())
                {
                    desc = $"{version.FormatDescriptionText()}";
                }
                else if (model.HasDescription())
                {
                    desc = $"{version.FormatTrainedWords()}{model.FormatDescriptionText()}";
                }
                else
                {
                    desc = version.FormatTrainedWords();
                }

                if(!string.IsNullOrEmpty(desc))
                    File.WriteAllText(versionInfoPath, desc);
            }


            if (!onlyMetadata)
            {
                bool hasMultipleFiles = version.files.Count > 1;
                for (int j = 0; j < version.files.Count; j++)
                {
                    ModelFile file = version.files[j];
                    string saveName = $"{versionName}{(hasMultipleFiles ? $"({j})" : "")}{IOUtil.ExtractExtension(file.name)}";
                    using (var b = AsyncHelper.Wait)
                    {
                        b.Run(DownloadFileAsync(file.downloadUrl, Path.Combine(path, saveName), saveName, simulate, downloadTimeout), (bool t) => { if (!t) { result = false; } });
                    }

                    //string meta = "null";
                    //if (file.metadata != null)
                    //{
                    //    meta = $"Format: {file.metadata.format}\nFP: {file.metadata.fp}\rSize: {file.metadata.size}\nPrimary: {file.primary}";
                    //}
                    //Console.WriteLine(meta);
                }
            }

            for (int j = 0; j < version.images.Count; j++)
            {
                Image img = version.images[j];
                string downloadUrl = img.GetSizedImageUrl(imageResolution);
                string urlDecoded = WebUtility.UrlDecode(img.url);
                string imgName = $"{versionName}_{urlDecoded.Substring(urlDecoded.LastIndexOf('/') + 1)}";
                using (var b = AsyncHelper.Wait)
                {
                    b.Run(DownloadFileAsync(downloadUrl, Path.Combine(path, imgName), imgName, simulate, downloadTimeout), (bool t)=> { if (!t) { result = false; } });
                }

                if (img.HasMeta() && !simulate)
                {
                    string promptName = imgName.Substring(0, imgName.LastIndexOf('.'));
                    string promptPath = Path.Combine(path, promptName + ".txt");
                    if (!File.Exists(promptPath))
                        File.WriteAllText(promptPath, img.FormatMeta());
                }
            }

            return result;
        }

        static async Task<bool> DownloadFileAsync(string url, string dest, string name, bool simulate, int downloadTimeout)
        {
            bool result = true;

            if (File.Exists(dest))
            {
                Console.Write($"\r{name} - ");
                ConsoleUtil.Write($"File Existed", WarnColor);
                if(simulate)
                    ConsoleUtil.Write($"(Simulated)", InfoColor);
                Console.WriteLine();
                return result;
            }

            if (simulate)
            {
                Console.Write($"\r{name} - ");
                ConsoleUtil.Write($"Success", SuccessColor);
                ConsoleUtil.Write($"(Simulated)", InfoColor);
                Console.WriteLine();
                return result;
            }

            bool progressChanged = false;
            void progressChange(object s, DownloadProgress e)
            {
                progressChanged = true;

                long bytesTotal = e.BytesTotal != 0 ? e.BytesTotal : e.BytesDownloaded;

                StringBuilder sb = new StringBuilder();
                if (bytesTotal >= (1024 * 1024))
                    sb.Append($" - [{e.BytesDownloaded / 1024 / 1024:0000}/{bytesTotal / 1024 / 1024:0000}]MB");

                if (e.BytesPerSec != 0)
                    sb.Append($" - {InternetUtil.FormatBytesPerSec(e.BytesPerSec)}");

                string detailsText = sb.ToString();
                int detailsLen = detailsText.Length;

                string destName = e.Destination.Substring(e.Destination.LastIndexOf(Path.DirectorySeparatorChar) + 1);
                int writeLen = destName.Length + detailsLen;
                if (writeLen > Console.BufferWidth && destName.Contains("."))
                {
                    string extension = destName.Substring(destName.LastIndexOf('.'));
                    int freeLen = Console.BufferWidth - (extension.Length + detailsLen + 2 + 2);
                    destName = destName.Substring(0, freeLen) + ".." + extension;
                }

                Console.Write($"\r{destName}{detailsText}");

            }

            DownloadResult downloadResult = null;
            Progress<DownloadProgress> progress = new Progress<DownloadProgress>();
            progress.ProgressChanged += progressChange;
            try
            {
                downloadResult = await HttpClientUtil.DownloadFileWithProgress(url, dest, progress, timeoutMS: downloadTimeout, reqHeaders: HasAuthToken() ? new List <Tuple<string, string>> { CreateAuthHeader() } : null, userAgent: userAgent);
            }
            finally
            {
                progress.ProgressChanged -= progressChange;

                if (!progressChanged)
                {
                    string destName = downloadResult.Destination.Substring(downloadResult.Destination.LastIndexOf(Path.DirectorySeparatorChar) + 1);
                    Console.Write($"\r{destName}");
                }

                Console.Write(" - ");
                if (downloadResult.Success)
                    ConsoleUtil.Write($"Success", SuccessColor);
                else
                {
                    if (downloadResult.Existed)
                    {
                        ConsoleUtil.Write($"File Existed", WarnColor);
                    }
                    else
                    {
                        result = false;
                        ConsoleUtil.Write(downloadResult.Error, ErrorColor);
                        if (downloadResult != null && File.Exists(downloadResult.Destination))
                        {
                            File.Delete(downloadResult.Destination);
                        }
                    }
                }

                Console.WriteLine();
            }

            return result;
        }

        static Tuple<string, string> CreateAuthHeader()
        {
            return new Tuple<string, string>("Authorization", $"Bearer {authToken}");
        }

        static bool HasAuthToken()
        {
            return authToken != null;
        }

        static bool SkipFromTypesMatch(string key)
        {
            bool skipIncludeType = includeType.Count > 0 && !includeType.Contains(key, StringComparer.OrdinalIgnoreCase);
            bool skipExcludeType = excludeType.Count > 0 && excludeType.Contains(key, StringComparer.OrdinalIgnoreCase);
            return skipIncludeType || skipExcludeType;
        }

        static bool SkipFromTagMatch(List<string> tags)
        {
            bool skipIncludeTag = includeTag.Count > 0 && !ContainsTag(includeTag, tags);
            bool skipExcludeTag = excludeTag.Count > 0 && ContainsTag(excludeTag, tags);
            return skipIncludeTag || skipExcludeTag;
        }

        static bool ContainsTag(List<string> filter, List<string> inTags)
        {
            for (int i = 0; i < filter.Count; i++)
            {
                for (int j = 0; j < inTags.Count; j++)
                {
                    if(filter[i].Equals(inTags[j], StringComparison.OrdinalIgnoreCase))
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        static string FormatModelPath(ModelInfo model)
        {
            string pathName = pathPattern;
            pathName = pathName.Replace("{id}", $"{model.id}");
            pathName = pathName.Replace("{name}", $"{model.FormatName()}");
            pathName = pathName.Replace("{creator}", $"{model.creator.username}");

            return @"\\?\" + Path.Combine(workingDir, model.type, IOUtil.RemoveIllegalPathCharacters(pathName));
        }

        static string FormatModelName(ModelInfo model, ModelVersion version)
        {
            string modelName = modelPattern ;
            modelName = modelName.Replace("{id}", $"{version.id}");
            modelName = modelName.Replace("{name}", $"{model.FormatName()}");
            modelName = modelName.Replace("{creator}", $"{model.creator.username}");
            modelName = modelName.Replace("{version}", $"{version.FormatName()}");

            return IOUtil.RemoveIllegalPathCharacters(modelName);
        }

        static string FormatFileName(ModelInfo model, ModelVersion version, ModelFile file)
        {
            int lastPeriodIndex = file.name.LastIndexOf('.');
            string extension = lastPeriodIndex == -1 ? "" : file.name.Substring(lastPeriodIndex);
            string fileNameNoExt = lastPeriodIndex == -1 ? file.name : file.name.Substring(0, lastPeriodIndex);
            string fileName = filePattern;
            fileName = fileName.Replace("{id}", $"{version.id}");
            fileName = fileName.Replace("{name}", $"{model.FormatName()}");
            fileName = fileName.Replace("{creator}", $"{model.creator.username}");
            fileName = fileName.Replace("{version}", $"{version.FormatName()}");
            fileName = fileName.Replace("{filename}", $"{fileNameNoExt}");

            return IOUtil.RemoveIllegalPathCharacters($"{fileName}{extension}");
        }

        static void PrintModeInfo()
        {
            ConsoleUtil.WriteLine($"[Running in {mode} mode]", ConsoleColor.DarkCyan);

            PrintKeyValue("Working Directory", workingDir);

            if (mode == OperatingMode.Validate)
            {
                PrintListIfNotEmpty("Include Types", includeType);
                PrintListIfNotEmpty("Exclude Types", excludeType);
            }
            else if(mode == OperatingMode.Download || mode == OperatingMode.Clipboard || mode == OperatingMode.Monitor)
            {
                //ConsoleUtil.Write("Model Preferences: ", KeyColor);
                //ConsoleUtil.Write(modelPreference.ToString(), ValueColor);
                //Console.WriteLine();

                PrintKeyValue("Image Width", imageResolution > 0 ? imageResolution.ToString() : "Max");
                PrintKeyValue("AuthToken", HasAuthToken() ? authToken.ToString() : "None", HasAuthToken() ? ValueColor : ErrorColor);

                if (mode == OperatingMode.Clipboard)
                    PrintKeyValue("Clipboard Mode", clipboardMode);

                if (mode == OperatingMode.Monitor)
                {
                    PrintKeyValue("Monitor Delay: ", monitorDelay.ShortFormat());
                    PrintListIfNotEmpty("Include Types", includeType);
                    PrintListIfNotEmpty("Exclude Types", excludeType);
                    PrintListIfNotEmpty("Include Tags", includeTag);
                    PrintListIfNotEmpty("Exclude Tags", excludeTag);
                }

                if (downloadOnlyMetadata)
                    ConsoleUtil.WriteLine("Only downloading metadata", WarnColor);
            }
        }

        static void PrintListIfNotEmpty(string header, List<string> list)
        {
            if (list.Count > 0)
            {
                ConsoleUtil.Write($"{header}: ", KeyColor);
                ConsoleUtil.Write(string.Join(", ", list), ValueColor);
                Console.WriteLine();
            }
        }

        static void PrintKeyValue(string key, object value, ConsoleColor? valueColor = null) => PrintKeyValue(key, value.ToString(), valueColor);
        static void PrintKeyValue(string key, string value, ConsoleColor? valueColor = null)
        {
            ConsoleUtil.Write($"{key}: ", KeyColor);
            ConsoleUtil.Write(value.ToString(), valueColor.HasValue ? valueColor.Value : ValueColor);
            Console.WriteLine();
        }

        static void PrintHelp()
        {
            ConsoleUtil.WriteLine("Usage: CivitaiCLI /path=\"\" /mode=monitor", InfoColor);
            Console.WriteLine();
            ConsoleUtil.Write("/path=\"\"", ValueColor);
            ConsoleUtil.WriteLine(" - Path to download models to, can be relative or absolute", KeyColor);
            Console.WriteLine();
            ConsoleUtil.Write("/mode=mode", ValueColor);
            ConsoleUtil.WriteLine(" - Operating mode, defaults to interactive", KeyColor);
            ConsoleUtil.Write("\tInteractive", ValueColor);
            ConsoleUtil.WriteLine("\t- Ask the user which mode to use", KeyColor);
            ConsoleUtil.Write("\tDownload", ValueColor);
            ConsoleUtil.WriteLine("\t- Paste in urls to download", KeyColor);
            //ConsoleUtil.Write("TODO", WarnColor);
            ConsoleUtil.Write("\tClipboard", ValueColor);
            ConsoleUtil.WriteLine("\t- Watch clipboard for valid urls, add to a queue or download immediately", KeyColor);
            ConsoleUtil.Write("\tMonitor", ValueColor);
            ConsoleUtil.WriteLine("\t\t- Monitor the API for new models, download metadata/models automatically", KeyColor);
            ConsoleUtil.Write("\tValidate", ValueColor);
            ConsoleUtil.WriteLine("\t- Poll the api for nuked models and move them to \"Nuked\"", KeyColor);
            Console.WriteLine();
            ConsoleUtil.Write("/imagewidth=450", ValueColor);
            ConsoleUtil.WriteLine("\t\t- Maximum image width, specify the maximum width (or height for landscape) of images to be downloaded (0 = original)", KeyColor);
            Console.WriteLine();
            ConsoleUtil.Write("/modelpattern=\"{name}_{version}\"", ValueColor);
            ConsoleUtil.WriteLine("\t- Format model name, valid replacements: id,name,creator,version", KeyColor);
            ConsoleUtil.Write("/pathpattern=\"{id}_{name}\"", ValueColor);
            ConsoleUtil.WriteLine("\t\t- Format model path, valid replacements: id,name,creator", KeyColor);
            Console.WriteLine();
            ConsoleUtil.Write("/downloadtimeout=30", ValueColor);
            ConsoleUtil.WriteLine("\t- Number of seconds of inactivity after which to cancel the download attempt", KeyColor);
            ConsoleUtil.Write("/authtoken=\"TOKEN\"", ValueColor);
            ConsoleUtil.WriteLine("\t- Specify a civitai user token to use for downloading models (for models that require being logged in)", KeyColor);
            Console.WriteLine();
            ConsoleUtil.Write("/monitordelay=300", ValueColor);
            ConsoleUtil.WriteLine("\t- Delay in seconds between updates", KeyColor);
            ConsoleUtil.Write("/monitormodelcount=20", ValueColor);
            ConsoleUtil.WriteLine("\t- Number of models to fetch every update", KeyColor);
            ConsoleUtil.Write("/monitoralwaysupdate", ValueColor);
            ConsoleUtil.WriteLine("\t- Process models regardless of update time", KeyColor);
            Console.WriteLine();
            ConsoleUtil.Write("/downloadonlymetadata", ValueColor);
            ConsoleUtil.WriteLine("\t- Only download metadata", KeyColor);
            Console.WriteLine();
            ConsoleUtil.Write("/clipboardmode=mode", ValueColor);
            ConsoleUtil.WriteLine(" - Clipboard mode, defaults to download", KeyColor);
            ConsoleUtil.Write("\tDownload", ValueColor);
            ConsoleUtil.WriteLine("\t- Start downloading as they're copied", KeyColor);
            ConsoleUtil.Write("\tQueue", ValueColor);
            ConsoleUtil.WriteLine("\t\t- Add to queue, requiring interaction before starting download", KeyColor);
            Console.WriteLine();
            //ConsoleUtil.WriteLine("/modelpreference=pref1,pref2 - Model Preference, specify which file you want to download for every model version, defaults to none (download all)", ValueColor);
            //ConsoleUtil.WriteLine("\tNone", ValueColor);
            //ConsoleUtil.WriteLine("\tPrimary", ValueColor);
            //ConsoleUtil.WriteLine("\tPruned", ValueColor);
            //ConsoleUtil.WriteLine("\tFull", ValueColor);
            //ConsoleUtil.WriteLine("\tF16", ValueColor);
            //ConsoleUtil.WriteLine("\tF32", ValueColor);
            //Console.WriteLine();
            ConsoleUtil.Write("/includetype=lora,etc", ValueColor);
            ConsoleUtil.WriteLine("\t- Only process models of these types", KeyColor);
            ConsoleUtil.Write("/excludetype=other,etc", ValueColor);
            ConsoleUtil.WriteLine("\t- Only process models that don't match these types", KeyColor);
            ConsoleUtil.Write("/includetag=nsfw,etc", ValueColor);
            ConsoleUtil.WriteLine("\t- Only process models that have one of these tags", KeyColor);
            ConsoleUtil.Write("/excludetag=style,etc", ValueColor);
            ConsoleUtil.WriteLine("\t- Only process models lacking any of these tags", KeyColor);
            Console.WriteLine();
            ConsoleUtil.WriteLine("Supported Urls:", KeyColor);
            ConsoleUtil.WriteLine("\tmodel: https://civitai.com/models/MODELID", ValueColor);
            ConsoleUtil.WriteLine("\tmodelVersion: https://civitai.com/models/MODELID?modelVersionId=VERSIONID", ValueColor);
            failed = true;
        }
    }
}
